package org.group6.aufgabe2;

import de.dailab.graphworld.ontology.GraphWorldException;
import de.dailab.graphworld.ontology.Node;
import de.dailab.graphworld.world.GraphWorldAgentBean;
import de.dailab.jiactng.agentcore.action.AbstractMethodExposingBean;
import de.dailab.jiactng.agentcore.action.Action;
import de.dailab.jiactng.agentcore.action.ActionResult;
import de.dailab.jiactng.agentcore.ontology.IActionDescription;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.Stack;
import org.group6.aufgabe2.memory.PathInfo;
import org.group6.aufgabe2.memory.StateInfo;
import org.group6.utils.AgentUtils;

/**
 *
 * @author Kseniya
 */
public class BeanMoving extends AbstractMethodExposingBean {

    private IActionDescription actionInit;
    private IActionDescription actionExplore;
    private IActionDescription actionMove;
    private IActionDescription actionTakeGold;
    private IActionDescription actionDropGold;
    private int homeNodeId = 0;
    private int worldSize;
    private String teamName;
    private final Queue<Node> moves = new LinkedList<>();

    @Override
    public void doStart() throws Exception {
        actionInit = thisAgent.searchAction(new Action(GraphWorldAgentBean.ACTION_INIT_TEAM_POSITION));
        actionExplore = thisAgent.searchAction(new Action(GraphWorldAgentBean.ACTION_EXPLORE));
        actionMove = thisAgent.searchAction(new Action(GraphWorldAgentBean.ACTION_MOVE));
        actionTakeGold = thisAgent.searchAction(new Action(GraphWorldAgentBean.ACTION_TAKE_GOLD));
        actionDropGold = thisAgent.searchAction(new Action(GraphWorldAgentBean.ACTION_DROP_GOLD));
    }

    @Override
    public void doCleanup() throws Exception {
        // clean up agent's memory
        memory.removeAll(new PathInfo(null, null));
        memory.removeAll(new StateInfo(null, null));
        // TODO check if necessary
        actionExplore = null;
        actionMove = null;
        actionDropGold = null;
        actionTakeGold = null;
    }

    @Override
    public void execute() {
        StateInfo state = memory.read(new StateInfo(null, null));
        Node current = readCurrNode();
        // check if task finished
        if (state != null && state.finished && current.id.intValue() == homeNodeId) {
            if (!state.informed) {
//                log.warn("Agent " + thisAgent.getAgentName() + " has finished its task.");
                memory.update(new StateInfo(null, null), new StateInfo(null, Boolean.TRUE));
            }
            return;
        }
        // check if we are initialised
        if (state == null) {
            initialise();
        } else if (!moves.isEmpty()) { // execute queued move action
            move(moves.element());
        }
    }

    /**
     * Reads the node we remember standing on.
     *
     * @return first node in stored path
     */
    private Node readCurrNode() {
        PathInfo storedPath = memory.read(new PathInfo(null, null));
        if (storedPath == null || storedPath.path.isEmpty()) {
            return null;
        }
        return storedPath.path.peek();
    }

    @Override
    public void receiveResult(ActionResult result) {

        if (result.getFailure() != null) {
            // this can happen when many agents on the same node grab few gold
            // so there already is no gold when agent's turn is
            if (result.getFailure() instanceof GraphWorldException
                    && ((GraphWorldException) result.getFailure()).getMessage().contains("location does not contain any gold")) {
                explore();
            }

            // print only errors that are not caused by illegal move - we'll 
            // simply try the move again in this case because the queued move 
            // won't be removed
            if (!((GraphWorldException) result.getFailure()).getMessage().contains("Illegal move")) {
//                log.warn("FAILURE!!! while executing action " + result.getAction().getName()
//                        + "\n\t\t\t\t\t\t\t\t\t\t\t\t\tFailure details: " + result.getFailure());
//                PathInfo read = memory.read(new PathInfo(null, null));
//                log.warn("stored path info: goingHome = " + read.goingHome);
//                log.warn("Path itself: " + read.path);
            }
            return;
        }

        String actionName = result.getAction().getName();
        switch (actionName) {
            case GraphWorldAgentBean.ACTION_INIT_TEAM_POSITION:
                // store the initial position
                Node curr = (Node) result.getResults()[0];
                initMemory(curr);
                explore();
                break;
            case GraphWorldAgentBean.ACTION_EXPLORE:
                Set<Node> neighbours = new HashSet<>((Set<Node>) result.getResults()[0]);
                consumeExplore(neighbours);
                break;
            case GraphWorldAgentBean.ACTION_MOVE:
                Node movedTo = (Node) result.getResults()[0];
                consumeMove(movedTo);
                break;
            case GraphWorldAgentBean.ACTION_TAKE_GOLD:
                consumeGoldTake();
                break;
            case GraphWorldAgentBean.ACTION_DROP_GOLD:
                consumeGoldDrop();
                break;
            default:
                break;
        }
        super.receiveResult(result);
    }

    /**
     * Drops gold pheromones on target node.
     *
     * @param target to drop pheromones on.
     */
    protected void dropGoldPheromones(Node target) {
        // this impl drops no pheromones...
    }

    protected Node selectMoveTarget(Collection<Node> explored) {
        // check if any explored node has gold on it
        for (Node node : explored) {
            if (node.getGold() > 0) {
                return node;
            }
        }
        // or select random node
        return explored.iterator().next();
    }

    /**
     * removes the node the agent is currently on, from explored nodes and
     * refreshes the remembered path info with it.
     *
     * @param explored nodes returned by explore action
     */
    private void validateExplored(Collection<Node> explored) {
        Node currStored = readCurrNode();
        for (Node node : explored) {
            if (node.getID() == currStored.getID()) {
                // current node found, replace last stored node with it
                PathInfo pathInfo = memory.read(new PathInfo(null, null));
                pathInfo.path.pop();
                pathInfo.path.push(node);
                memory.update(new PathInfo(null, null), pathInfo);
                // we don't want to select next move to the node we are already on
                explored.remove(node);
                break;
            }
        }
    }

    /**
     * Checks if all nodes are visited at least once.
     *
     * @return true if all nodes visited
     */
    private boolean allNodesVisited() {
        PathInfo storedPath = memory.read(new PathInfo(null, null));
        Set<Integer> ids = new HashSet<>();
        for (Node node : storedPath.path) {
            ids.add(Integer.valueOf(node.getID()));
        }
        return ids.size() == worldSize;
    }

    /**
     * inits all essential info to remember into memory. Only the initial
     * position we are on after init action can be stored now.
     *
     * @param initialPosition to remember in path info
     */
    private void initMemory(Node initialPosition) {
        Stack<Node> path = new Stack<>();
        path.push(initialPosition);
        memory.write(new PathInfo(Boolean.FALSE, path));
        memory.write(new StateInfo(Boolean.FALSE, Boolean.FALSE));
    }

    /**
     * should be called after successful explore only. Updates memory, selects
     * next node to move to and queues an appropriate move action.
     *
     * @param explored set of explored nodes
     */
    private void consumeExplore(Set<Node> explored) {
        validateExplored(explored);
        // neighbours is a set, so the randomness of first 
        // element selection is guaranteed by default
        Node selected = selectMoveTarget(explored);
        // queue next move
        queueNextMove(selected);
    }

    /**
     * should be called after successful move only. Updates memory, selects and
     * invokes next action to be done.
     *
     * @param movedTo node we are currently on (returned by move action)
     */
    private void consumeMove(Node movedTo) {
        // remove first queued move since already done now
        moves.remove();
        // begin with updating memory and sleecting next action...
        PathInfo pathInfo = memory.read(new PathInfo(null, null));
        // check if already going home or looking for gold
        if (!pathInfo.goingHome) {
            // add the node to path
            pathInfo.path.push(movedTo);
            if (movedTo.getGold() > 0) {
                // if not, either take gold if found one...
                takeGold();
            } else if (allNodesVisited()) { // check if we visited all nodes once
                // => we are finished and must go home
                pathInfo.goingHome = Boolean.TRUE;
                memory.update(new StateInfo(null, null), new StateInfo(Boolean.TRUE, null));
                // remove cycles to speed up the returning
                List<Node> pathWithoutCycles = AgentUtils.optimizePath(pathInfo.path);
                pathInfo.path = new Stack<>();
                pathInfo.path.addAll(pathWithoutCycles);
                // remember the shortened path
                memory.update(new PathInfo(null, null), pathInfo);
                // move one node back home
                Node oneNodeBackHome = pathInfo.path.get(pathInfo.path.size() - 2);
                queueNextMove(oneNodeBackHome);
            } else {
                // ...or explore node for the next move
                explore();
            }
        } else { // agent is going home
            StateInfo state = memory.read(new StateInfo(null, null));
            Node pheromoneDropTarget = pathInfo.path.pop();
            // if we are finished, there is no point of dropping pheromones since
            // we went through all nodes and didn't find any gold
            if (!state.finished) {
                // drop pheromones to last stored node (the one we came from)
                dropGoldPheromones(pheromoneDropTarget);
            }
            // replace the old stored node we were previously on and are heading to with the "new version"
            pathInfo.path.pop();
            pathInfo.path.push(movedTo);
            // if landed on home node, drop gold. If we are finished, it 
            // also indicates that we do not have any gold with us so no drop
            if (movedTo.id.intValue() == homeNodeId) {
                if (!state.finished) {
                    dropGold();
                }
            } else {
                // move one node back home
                Node oneNodeBackHome = pathInfo.path.get(pathInfo.path.size() - 2);
                queueNextMove(oneNodeBackHome);
            }
        }
        // always update remembered path
        memory.update(new PathInfo(null, null), pathInfo);
    }

    /**
     * should be called after successful gold take only. Updates memory, selects
     * and invokes next action to be done.
     */
    private void consumeGoldTake() {
        // we are going home now since gold taken successfully
        PathInfo pathInfo = memory.read(new PathInfo(null, null));
        pathInfo.goingHome = Boolean.TRUE;
        // remove cycles to speed up the returning
        List<Node> pathWithoutCycles = AgentUtils.optimizePath(pathInfo.path);
        pathInfo.path = new Stack<>();
        pathInfo.path.addAll(pathWithoutCycles);
        // remember the shortened path
        memory.update(new PathInfo(null, null), pathInfo);
        // move one node back home
        Node oneNodeBackHome = pathInfo.path.get(pathInfo.path.size() - 2);
        queueNextMove(oneNodeBackHome);
    }

    /**
     * should be called after successful gold drop only. Updates memory, selects
     * and invokes next action to be done.
     */
    private void consumeGoldDrop() {
        // we are not going home from now on since already home,
        // start the whole gold search again
        memory.update(new PathInfo(null, null), new PathInfo(Boolean.FALSE, null));
        explore();
    }

    private void initialise() {
        this.invoke(actionInit, new Serializable[]{getTeamName()}, this);
    }

    private void explore() {
        this.invoke(actionExplore, null, this);
    }

    private void queueNextMove(Node node) {
        moves.offer(node);
    }

    private void move(Node target) {
        this.invoke(actionMove, new Serializable[]{target}, this);
    }

    private void takeGold() {
        invoke(actionTakeGold, null, this);
    }

    private void dropGold() {
        invoke(actionDropGold, null, this);
    }

    public int getWorldSize() {
        return worldSize;
    }

    public void setWorldSize(int worldSize) {
        this.worldSize = worldSize;
    }

    public String getTeamName() {
        return teamName;
    }

    public void setTeamName(String teamName) {
        this.teamName = teamName;
    }
}
